﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using RecommenderSystem;
using RecommenderSystem.misc;

//using log4net;

namespace RecommenderSystem.algorithms.modelBased
{
    class SVD: ModelBased
    {
        //Caching users similarity
        protected Cache similarityCache;

        private IDictionary<string, KeyValuePair< Vector, double>> _usersModel;
        private IDictionary<string, KeyValuePair< Vector, double>> _itemsModel;
        private double mu;
        private int _cFeatures;
        public readonly string ALGORITHM_NAME = "SVD";

        public SVD(Dictionary<string, User> users, ICollection<IDataRecord> dataset, Dictionary<string, Cache> cache)
            : base(users, dataset) 
        {

            if (!cache.ContainsKey(ALGORITHM_NAME))
                cache.Add(ALGORITHM_NAME, new Cache());

            similarityCache = cache[ALGORITHM_NAME];

        }

        public void TrainBaseModel(int cFeatures)
        {
            int trainsCounter = Config.BEST_OF_TRAIN_BASE_MODEL;
            double bestRmse = -1d;
    trainBaseModelAgain:
            List<IDataRecord> validationDB = new List<IDataRecord>();
            List<IDataRecord> trainDB = new List<IDataRecord>();
            RecommenderSystem.split(_dataset, validationDB, trainDB, 1 - Config.VALIDATION_SET_SIZE);
            this._cFeatures = cFeatures;

            //initialize constant values.
            this.mu = calculateMu(trainDB);
            double gamma = generateGamma();
            double lambda = generateLambda();
            IDictionary<string, KeyValuePair< Vector, double>> users;
            IDictionary<string, KeyValuePair< Vector, double>> items;

            initializeUsersDictionary(out users, trainDB, cFeatures);
            initializeItemsDictionary(out items, trainDB, cFeatures);

            double eui, oldBu, oldBi, newBu, newBi, itemRank, oldRmse, newRmse;
            Vector oldPu, oldQi, newPu, newQi;
            string itemId, userId;
            oldRmse = calculateRMSE(validationDB, items, users);
            int iterCount = 0;
    iteration:
            foreach (IDataRecord record in trainDB)
            {
                itemId = record.getItemID();
                userId = record.getUserID();
                itemRank = double.Parse( record.getItemRank() );

                oldBu = users[userId].Value;
                oldBi = items[itemId].Value;
                oldPu = users[userId].Key;
                oldQi = items[itemId].Key;

                eui = itemRank - mu - oldBi - oldBu - (oldPu * oldQi);
                newBu = oldBu + lambda * (eui - gamma * oldBu);
                newBi = oldBi + lambda * (eui - gamma * oldBi);
                newQi = oldQi + lambda * (eui * oldPu - gamma * oldQi);
                newPu = oldPu + lambda * (eui * oldQi - gamma * oldPu);

                users[userId] = new KeyValuePair<Vector, double>(newPu, newBu);
                items[itemId] = new KeyValuePair<Vector, double>(newQi, newBi);
            }
            
            newRmse = calculateRMSE(validationDB, items, users);
            if (newRmse < oldRmse)//will itearte until the rmse will grow. 
            {
                iterCount++;
                oldRmse = newRmse;
                goto iteration;
            }

            if (bestRmse == -1 || bestRmse > oldRmse)//Optimiztion 
            {
//                logger.Debug("RMSE changed from: " + Math.Round(bestRmse, 4) + " to: " + Math.Round(oldRmse, 4));
                bestRmse = oldRmse;
                this._usersModel = users;   //new Dictionary<string, KeyValuePair< Vector, double>>(users);
                this._itemsModel = items;   //new Dictionary<string, KeyValuePair< Vector, double>>(items);
            }
            trainsCounter--;
            if (trainsCounter > 0)
            {
                goto trainBaseModelAgain;
            }

            this.mu = calculateMu(this._dataset);
//            logger.Info("Final RMSE: " + bestRmse);
        }

        public override double predictRating(string userID, string itemID)
        {
            if (!(_usersModel.Keys.Contains(userID)))
                _usersModel.Add(userID, new KeyValuePair<Vector, double>(new Vector(this._cFeatures), DbUtils.randomizeNumber()));
            var user = _usersModel[userID];
            if (!(_itemsModel.Keys.Contains(itemID)))
                _itemsModel.Add(itemID, new KeyValuePair<Vector, double>(new Vector(this._cFeatures), DbUtils.randomizeNumber()));
            var item = _itemsModel[itemID];
            return mu + user.Value + item.Value + (user.Key * item.Key);
        }

        public override double calculateSimilarity(User user, User otherUser)
        {
            try
            {
                if (similarityCache.isCached(user.getUserId(), otherUser.getUserId()))
                {
                    return similarityCache.retrieveFromCache(user.getUserId(), otherUser.getUserId());
                }

                double usersSimilarity = Vector.euclideanDistance(_usersModel[user.getUserId()].Key, _usersModel[otherUser.getUserId()].Key);
                similarityCache.addToCache(user.getUserId(), otherUser.getUserId(), usersSimilarity);
                similarityCache.addToCache(otherUser.getUserId(), user.getUserId(), usersSimilarity);

                return usersSimilarity;
            }
            catch (Exception ex)
            {
 //               logger.Error("Failed to calculate similarity, reason: ", ex);
                return 0d;
            }
        }
        
        
        /**
         * This method calculates the average item rank in the given dataset.
         * 
         */
        private double calculateMu(ICollection<IDataRecord> dataset)
        {
            if (dataset == null) { return 0d; }
            return dataset.Average(record => double.Parse(record.getData(Config.ITEM_RANK)));
        }

        private double generateLambda()
        {
            return 0.05;
        }
        private double generateGamma()
        {
            return 0.05;
        }

        private void initializeUsersDictionary(out IDictionary<string, KeyValuePair<Vector, double>> users, List<IDataRecord> trainDB , int cFeatures)
        {
            users = new Dictionary<string, KeyValuePair<Vector, double>>();
            var userlist = trainDB.Select((rec) => rec.getUserID()).Distinct();
            foreach (string user in userlist)
            {
                Vector pu = new Vector(cFeatures);
                double bu = misc.DbUtils.randomizeNumber();
                users.Add(user, new KeyValuePair<Vector, double>(pu, bu));
            }
        }
        private void initializeItemsDictionary(out IDictionary<string, KeyValuePair<Vector, double>> items, List<IDataRecord> trainDB, int cFeatures)
        {
            items = new Dictionary<string, KeyValuePair<Vector, double>>();
            foreach (IDataRecord record in trainDB)
            {
                if (!items.Keys.Contains(record.getItemID()))
                {
                    Vector qi = new Vector(cFeatures);
                    double bi = misc.DbUtils.randomizeNumber();
                    items.Add(record.getItemID(), new KeyValuePair<Vector, double>(qi, bi));
                }

            }
        }

        private double calculateRMSE(List<IDataRecord> validationDB, IDictionary<string, KeyValuePair<Vector, double>> items, IDictionary<string, KeyValuePair<Vector, double>> users)
        {
            double rmse = 0d;
            string itemId, userId;
            double itemRate, predictedRate;
            foreach (IDataRecord record in validationDB)
            {
                itemId = record.getItemID();
                userId = record.getUserID();
                itemRate = double.Parse(record.getItemRank());

                if (users.ContainsKey(userId) & items.ContainsKey(itemId))
                {
                    predictedRate = mu + users[userId].Value + items[itemId].Value + (users[userId].Key * items[itemId].Key);
                    rmse += Math.Pow(predictedRate - itemRate, 2);
                }

            }

            return validationDB.Count > 0 ? Math.Sqrt(rmse / validationDB.Count) : 0d;
        }


    }
}
